package ftpproxy;

import static ftpproxy.Constants.CRLF;
import static ftpproxy.Constants.pwDebug;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.BindException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

public class ClientHandler implements Runnable {
	final static Map<int[], Integer> lastPorts = new HashMap<int[], Integer>();

	Socket skControlServer;
	BufferedReader brClient, brServer;
	PrintStream psClient, osServer;

	ServerSocket ssDataClient, ssDataServer;
	Socket skDataClient, skDataServer;

	// IP of interface facing client and server respectively
	String sLocalClientIP;
	String sLocalServerIP;

	DataConnect dcData;
	boolean serverPassive = false;
	boolean userLoggedIn = false;
	boolean connectionClosed = false;

	final static String server2proxy = "S->P: ";
	final static String proxy2server = "S<-P: ";
	final static String proxy2client = "P->C: ";
	final static String client2proxy = "P<-C: ";
	final static String server2client = "S->C: ";
	final static String client2server = "S<-C: ";

	// DB Info
	DatabaseInfo dbInfo = new DatabaseInfo();

	// Currecnt Directory
	String curDir;

	Socket skControlClient;

	// private final Configuration config;
	private Configuration config;

	public ClientHandler(Configuration config, Socket skControlClient) {
		this.config = config;
		this.skControlClient = skControlClient;
	}

	public void run() {
		try {
			brClient = new BufferedReader(new InputStreamReader(skControlClient.getInputStream()));
			psClient = new PrintStream(skControlClient.getOutputStream());

			// config.allowFrom(LIST)
			// config.denyFrom(LIST)

			if ((config.allowFrom != null && !isInSubnetList(config.allowFrom, skControlClient.getInetAddress()))
					|| isInSubnetList(config.denyFrom, skControlClient.getInetAddress())) {

				String toClient = config.msgOriginAccessDenied;
				psClient.print(toClient + CRLF);
				if (config.debug)
					Log.write(config, proxy2client + toClient);

				skControlClient.close();
				return;
			}

			// IP Masquerading
			try {
				if (config.masqueradeHostname == null) {
					sLocalClientIP = skControlClient.getLocalAddress().getHostAddress().replace('.', ',');
				} else {
					sLocalClientIP = InetAddress.getByName(config.masqueradeHostname.trim()).getHostAddress()
							.replace('.', ',');
				}

			} catch (UnknownHostException e) {
				String toClient = config.msgMasqHostDNSError;
				psClient.print(toClient + CRLF);

				if (config.debug)
					Log.write(config, proxy2client + toClient);

				skControlClient.close();
				return;
			}

			@SuppressWarnings("unused")
			String username = null;
			String hostname = null;
			int serverport = 21;

			if (config.onlyAuto && config.autoHostname != null) {
				username = null; // value will not be used
				hostname = config.autoHostname;
				serverport = config.autoPort;

			} else {
				if (config.onlyAuto) { // and autoHostname == null
					throw new RuntimeException("only_auto is enabled, but no auto_host is set");
				}

				String toClient = config.msgConnect;
				psClient.print(toClient + CRLF);
				psClient.flush();
				if (config.debug)
					Log.write(config, proxy2client + toClient);

				// the username is read from the client
				String fromClient = brClient.readLine();
				if (config.debug)
					Log.write(config, client2proxy + fromClient);

				String userString = fromClient.substring(5);

				int a = userString.indexOf('@');
				int c = userString.lastIndexOf(':');

				if (a == -1 && config.isUrlSyntaxEnabled) {
					int a1 = userString.indexOf('*');
					if (a1 != -1) {
						a = a1;
						c = userString.lastIndexOf('*');
						if (c == a)
							c = -1;
					}
				}
				if (a == -1) {
					username = userString;
					hostname = config.autoHostname;
					serverport = config.autoPort;
				} else if (c == -1) {
					username = userString.substring(0, a);
					hostname = userString.substring(a + 1);
				} else {
					username = userString.substring(0, a);
					hostname = userString.substring(a + 1, c);
					serverport = Integer.parseInt(userString.substring(c + 1));
				}
			}

			// don't know which host to connect to
			if (hostname == null) {
				String toClient = config.msgIncorrectSyntax;
				if (config.debug)
					Log.write(config, proxy2client + toClient);
				psClient.print(toClient + CRLF);
				skControlClient.close();
				return;
			}

			InetAddress serverAddress = InetAddress.getByName(hostname);

			if ((config.allowTo != null && !isInSubnetList(config.allowTo, serverAddress))
					|| isInSubnetList(config.denyTo, serverAddress)) {

				String toClient = config.msgDestinationAccessDenied;

				psClient.print(toClient + CRLF);
				skControlClient.close();
				return;
			}

			serverPassive = config.useActive != null && !isInSubnetList(config.useActive, serverAddress)
					|| isInSubnetList(config.usePassive, serverAddress);

			if (config.debug)
				Log.write(config, "Connecting to " + hostname + " on port " + serverport);

			// ���� FTP Server Socket ��...
			try {
				skControlServer = new Socket(serverAddress, serverport);

			} catch (ConnectException e) {
				String toClient = config.msgConnectionRefused;
				psClient.print(toClient + CRLF);
				psClient.flush();
				if (config.debug)
					Log.write(config, proxy2client + toClient);
				return;
			}

			brServer = new BufferedReader(new InputStreamReader(skControlServer.getInputStream()));
			osServer = new PrintStream(skControlServer.getOutputStream(), true);
			sLocalServerIP = skControlServer.getLocalAddress().getHostAddress().replace('.', ',');

			// 220 APPLE FTP server (Version 4.1 Sat Sep 7 14:31:53 CDT
			// 2002) ready.
			readResponseFromServer(true);

			for (;;) {
				String s = brClient.readLine();
				if (s == null) {
					break;
				}

				readCommandFromClient(s);

				// exit if connection closed (response == 221,421,530)
				if (connectionClosed) {
					break;
				}
			}

		} catch (Exception e) {
			String toClient = config.msgInternalError;
			if (config.debug) {
				Log.write(config, proxy2client + toClient + e.toString());
				e.printStackTrace(pwDebug);
			}
			psClient.print(toClient + CRLF);
			psClient.flush();

		} finally {
			if (ssDataClient != null && !config.clientOneBindPort) {
				try {
					ssDataClient.close();
				} catch (IOException ioe) {
				}
			}
			if (ssDataServer != null && !config.serverOneBindPort) {
				try {
					ssDataServer.close();
				} catch (IOException ioe) {
				}
			}
			if (skDataClient != null)
				try {
					skDataClient.close();
				} catch (IOException ioe) {
				}
			if (skDataServer != null)
				try {
					skDataServer.close();
				} catch (IOException ioe) {
				}
			if (psClient != null)
				psClient.close();
			if (osServer != null)
				osServer.close();
			if (dcData != null)
				dcData.close();
		}
	}

	private void readCommandFromClient(String fromClient) throws IOException {
		String cmd = fromClient.toUpperCase();
		String logCMD = "LOG";

		if (StringUtils.length(cmd) >= 4)
			logCMD = cmd.substring(0, 4);

		if (!userLoggedIn && (cmd.startsWith("PASV") || cmd.startsWith("PORT"))) {
			// do not process PASV if user has not logged in yet.
			psClient.print("530 Not logged in." + CRLF);
			psClient.flush();
			return;
		}

		if (cmd.startsWith("PASV")) {
			if (config.debug)
				Log.write(config, dbInfo.l_userid, logCMD, client2proxy + cmd);

			if (ssDataClient != null && !config.clientOneBindPort) {
				try {
					ssDataClient.close();
				} catch (IOException ioe) {
				}
			}
			if (skDataClient != null)
				try {
					skDataClient.close();
				} catch (IOException ioe) {
				}
			if (dcData != null)
				dcData.close();

			if (ssDataClient == null || !config.clientOneBindPort) {

				ssDataClient = getServerSocket(config.clientBindPorts, skControlClient.getLocalAddress());
			}

			if (ssDataClient != null) {
				int port = ssDataClient.getLocalPort();

				String toClient = "227 Entering Passive Mode (" + sLocalClientIP + "," + (int) (port / 256) + ","
						+ (port % 256) + ")";
				psClient.print(toClient + CRLF);
				psClient.flush();
				if (config.debug)
					Log.write(config, dbInfo.l_userid, logCMD, proxy2client + toClient);

				setupServerConnection(ssDataClient);

			} else {
				String toClient = "425 Cannot allocate local port..";
				psClient.print(toClient + CRLF);
				psClient.flush();
				if (config.debug)
					Log.write(config, dbInfo.l_userid, logCMD, proxy2client + toClient);
			}

		} else if (cmd.startsWith("PORT")) {
			int port = parsePort(fromClient);

			if (ssDataClient != null && !config.clientOneBindPort) {
				try {
					ssDataClient.close();
				} catch (IOException ioe) {
				}
				ssDataClient = null;
			}
			if (skDataClient != null)
				try {
					skDataClient.close();
				} catch (IOException ioe) {
				}
			if (dcData != null)
				dcData.close();

			if (config.debug)
				Log.write(config, dbInfo.l_userid, logCMD, client2proxy + fromClient);

			try {
				skDataClient = new Socket(skControlClient.getInetAddress(), port);

				String toClient = "200 PORT command successful.";
				psClient.print(toClient + CRLF);
				psClient.flush();
				
				if (config.debug)
					Log.write(config, dbInfo.l_userid, logCMD, proxy2client + toClient);

				setupServerConnection(skDataClient);

			} catch (IOException e) {
				String toClient = "425 PORT command failed - try using PASV instead.";
				psClient.print(toClient + CRLF);
				psClient.flush();
				if (config.debug)
					Log.write(config, dbInfo.l_userid, logCMD, proxy2client + toClient);

				return;
			}
		} else {
			@SuppressWarnings("unused")
			String password = "";
			@SuppressWarnings("unused")
			String userId = "";
			String fileName = "";
			String filePath = "";

			// User ID || Password Input.
			if (cmd.startsWith("USER")) {
				userId = fromClient.substring(5);
				/*
				 * dbInfo.setUserInfo(userId);
				 * 
				 * if (sUtil.isNull(dbInfo.l_userid)) fromClient = "USER " +
				 * dbInfo.l_userid; else fromClient = "USER " +
				 * config.autoUserid;
				 */
				fromClient = "USER " + config.autoUserid;
				// if (config.debug) Log.write(config, dbInfo, logCMD,
				// "userId : " + userId);
				// if (config.debug) Log.write(config, dbInfo, logCMD,
				// "user id : " + dbInfo.l_userid);
				// if (config.debug) Log.write(config, dbInfo, logCMD,
				// "config.autoUserid : " + config.autoUserid);
				if (config.debug)
					Log.write(config, dbInfo.l_userid, logCMD, client2proxy + fromClient);

			} else if (cmd.startsWith("PASS")) {
				password = fromClient.substring(5);
				/*
				 * if (!password.equals(dbInfo.l_passwd)) fromClient =
				 * "PASS " + dbInfo.l_passwd; else fromClient = "PASS " +
				 * config.autoPassword;
				 */
				fromClient = "PASS " + config.autoPassword;

				// if (config.debug) Log.write(config, dbInfo, logCMD,
				// "password : " + password);
				// if (config.debug) Log.write(config, dbInfo, logCMD,
				// "l_pass : " + dbInfo.l_passwd);
				if (config.debug)
					Log.write(config, dbInfo.l_userid, logCMD, client2proxy + fromClient);

			} else if (cmd.startsWith("CWD")) {
				curDir = fromClient.substring(4);

				if (config.debug)
					Log.write(config, dbInfo.l_userid, logCMD, client2proxy + curDir);

			} else if (cmd.startsWith("STOR") || cmd.startsWith("RETR") || cmd.startsWith("DELE")) {
				fileName = fromClient.substring(5);

				if (fileName.startsWith("/home/molaw"))
					filePath = fileName;
				else
					filePath = curDir + fileName;

				if (config.debug)
					Log.write(config, dbInfo.l_userid, logCMD, filePath);
			}
			osServer.print(fromClient + CRLF);
			osServer.flush();

			readResponseFromServer(true);
			// readResponseFromServer(false);
		}
	}

	private String readResponseFromServer(boolean forwardToClient) throws IOException {
		String fromServer = brServer.readLine();
		String firstLine = fromServer;

		if (config.debug)
			Log.write(config, dbInfo.l_userid, "MSG", fromServer);
		// 230-2 unsuccessful login attempt since last login.
		// 230-Last unsuccessful login: Mon Nov 3 18:00:38 2003 on ftp
		// 230-Last login: Mon Nov 3 16:19:00 2003 on ftp
		// 230 User www logged in. --> Response Code �� ���� 4��° �ڸ��� "-"
		// �ֳ� ��ķ� ����.

		int response = Integer.parseInt(fromServer.substring(0, 3));

		if (fromServer.charAt(3) == '-') {
			String multiLine = fromServer.substring(0, 3) + ' ';
			while (!fromServer.startsWith(multiLine)) {

				if (forwardToClient) {
					psClient.print(fromServer + CRLF);
					psClient.flush();
				}
				if (config.debug)
					Log.write(config, dbInfo.l_userid, "MSG1", (forwardToClient ? server2client : server2proxy) + fromServer);

				fromServer = brServer.readLine();
			}
		}

		// check for successful login
		// 230 User logged in, proceed.
		// 221 Service closing control connection.
		// Logged out if appropriate.
		// 421 Service not available, closing control connection.
		// This may be a reply to any command if the service knows it
		// must shut down.
		// 530 Not logged in.

		if (response == 230) {
			userLoggedIn = true;
		} else if (response == 221 || response == 421 || response == 530) {
			if (userLoggedIn) {
				connectionClosed = true;
			}
			userLoggedIn = false;
		}

		// 110 Restart marker reply.
		if (forwardToClient || response == 110) {
			psClient.print(fromServer + CRLF);
			psClient.flush();
		}

		if (config.debug)
			Log.write(config, dbInfo.l_userid, "MSG2", (forwardToClient ? server2client : server2proxy) + fromServer);

		if (response >= 100 && response <= 199) {
			firstLine = readResponseFromServer(true);
		}

		return firstLine;
	}

	private void setupServerConnection(Object s) throws IOException {
		if (skDataServer != null) {
			try {
				skDataServer.close();
			} catch (IOException ioe) {
			}
		}

		if (serverPassive) {
			String toServer = "PASV";
			osServer.print(toServer + CRLF);
			osServer.flush();
			if (config.debug)
				Log.write(config, proxy2server + toServer);

			String fromServer = readResponseFromServer(false);

			int port = parsePort(fromServer);

			if (config.debug)
				Log.write(config, "Server: " + skControlServer.getInetAddress() + ":" + port);

			skDataServer = new Socket(skControlServer.getInetAddress(), port);

			(dcData = new DataConnect(config, s, skDataServer)).start();
		} else {
			if (ssDataServer != null && !config.serverOneBindPort) {
				try {
					ssDataServer.close();
				} catch (IOException ioe) {
				}
			}

			if (ssDataServer == null || !config.serverOneBindPort) {
				ssDataServer = getServerSocket(config.serverBindPorts, skControlServer.getLocalAddress());
			}

			if (ssDataServer != null) {
				int port = ssDataServer.getLocalPort();
				String toServer = "PORT " + sLocalServerIP + ',' + (int) (port / 256) + ',' + (port % 256);

				osServer.print(toServer + CRLF);
				osServer.flush();
				if (config.debug)
					Log.write(config, dbInfo.l_userid, "MSG3", proxy2server + toServer);

				readResponseFromServer(false);

				(dcData = new DataConnect(config, s, ssDataServer)).start();

			} else {
				String toClient = "425 Cannot allocate local port.";
				psClient.print(toClient + CRLF);
				psClient.flush();
				if (config.debug)
					Log.write(config, proxy2client + toClient);
			}
		}
	}
	
	public boolean isInSubnetList(List<Subnet> list, InetAddress ia) {
		if (list == null)
			return false;

		for (Iterator<Subnet> iterator = list.iterator(); iterator.hasNext();) {
			Subnet subnet = iterator.next();

			if (subnet.isInSubnet(ia))
				return true;
		}
		return false;
	}
	
	public synchronized ServerSocket getServerSocket(int[] portRanges, InetAddress ia) throws IOException {
		ServerSocket ss = null;

		if (portRanges != null) {

			int i; // current index of portRanges array
			int port;

			Integer lastPort = (Integer) lastPorts.get(portRanges);
			if (lastPort != null) {
				port = lastPort.intValue();
				for (i = 0; i < portRanges.length && port > portRanges[i + 1]; i += 2)
					;
				port++;
			} else {
				port = portRanges[0];
				i = 0;
			}

			for (int lastTry = -2; port != lastTry; port++) {
				if (port > portRanges[i + 1]) {
					i = (i + 2) % portRanges.length;
					port = portRanges[i];
				}
				if (lastTry == -1) {
					lastTry = port;
				}
				try {
					ss = new ServerSocket(port, 1, ia);
					lastPorts.put(portRanges, new Integer(port));
					break;
				} catch (BindException e) {
					// port already in use
					// Log.write("getServerSocket Error : " + e.toString()) ;
				}
			}
		} else {
			ss = new ServerSocket(0, 1, ia);
		}
		return ss;
	}
	
	public int parsePort(String s) throws IOException {
		int port;
		try {
			int i = s.lastIndexOf('(');
			int j = s.lastIndexOf(')');
			if ((i != -1) && (j != -1) && (i < j)) {
				s = s.substring(i + 1, j);
			}

			i = s.lastIndexOf(',');
			j = s.lastIndexOf(',', i - 1);

			port = Integer.parseInt(s.substring(i + 1));
			port += 256 * Integer.parseInt(s.substring(j + 1, i));
		} catch (Exception e) {
			throw new IOException();
		}
		return port;
	}
}
